Ideam_Logo

Estaciones hidrometeorológicas


Introducción

En este cuadernillo (Notebook) aprenderemos:

  1. Introduccion a la red de monitoreo del IDEAM

  2. Cátalogo de estaciones de IDEAM

  3. Consulta de datos usando la plataforma datosabiertos.gov.co

  4. Consulta de datos de temperatura y precipitación

  5. Otros datos disponibles

Prerequisitos

Conceptos

Importancia

Notas

Introducción a Pandas

Necesario

lectura de datos tabulares

Introducción a Datetime

Necesario

Entender estampas de tiempo

Introducción a Cartopy

Necesario

Entender estampas de tiempo

Introducción a folium

Útil

Mapas interactivos

  • Tiempo de aprendizaje: 30 minutos

1. Catalogo nacional de estaciones de IDEAM

Según el catálogo de estaciones hidrometeorológicas de IDEAM, el pais cuenta con alrededor de 4.400 estaciones de diferentes categorias. En el siguiente cuadro se resume el estado de las estaciones por categoría de acuerdo a la PQR No. 20229050190832 (Enero de 2023)

Categoria

Activa

Mantenimiento

Suspendidas

Limnigráfica

287

109

106

Climátologica principal

215

60

92

Mareográfica

4

2

2

Pluviográfica

104

0

87

Limnimétrica

323

11

557

Climática Ordinaria

211

31

253

Agrometeorológica

51

4

57

Radio Sonda

6

2

2

Pluviométrica

1109

9

603

Meteorológica Especial

40

4

68

Sinóptica Principal

27

3

4

Sinóptica Secundaria

2

0

5

Total

2381

235

1866

Librerias

import pandas as pd
import cartopy.crs as ccrs
import cartopy.feature as feature
import matplotlib.pyplot as plt

2. Acceso al catalogo en bart.ideam.gov.co

El catalogo nacional de estaciones de IDEAM actualizado se encuentra disponible en el servidor Bart. Podemos leer el catálogo usando pandas.read_excel como se muestra a continuación:

df_cat = pd.read_excel('http://bart.ideam.gov.co/cneideam/CNE_IDEAM.xls')
#df_cat.head()
df_cat.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4497 entries, 0 to 4496
Data columns (total 21 columns):
 #   Column                Non-Null Count  Dtype         
---  ------                --------------  -----         
 0   OBJECTID              4497 non-null   int64         
 1   CODIGO                4497 non-null   int64         
 2   nombre                4497 non-null   object        
 3   CATEGORIA             4497 non-null   object        
 4   TECNOLOGIA            4497 non-null   object        
 5   ESTADO                4497 non-null   object        
 6   FECHA_INSTALACION     4496 non-null   datetime64[ns]
 7   altitud               4497 non-null   int64         
 8   latitud               4497 non-null   float64       
 9   longitud              4497 non-null   float64       
 10  DEPARTAMENTO          4497 non-null   object        
 11  MUNICIPIO             4497 non-null   object        
 12  AREA_OPERATIVA        4497 non-null   object        
 13  AREA_HIDROGRAFICA     4497 non-null   object        
 14  ZONA_HIDROGRAFICA     4497 non-null   object        
 15  observacion           1335 non-null   object        
 16  CORRIENTE             4497 non-null   object        
 17  FECHA_SUSPENSION      1844 non-null   datetime64[ns]
 18  SUBZONA_HIDROGRAFICA  4497 non-null   object        
 19  ENTIDAD               4497 non-null   object        
 20  subred                1143 non-null   object        
dtypes: datetime64[ns](2), float64(2), int64(3), object(14)
memory usage: 737.9+ KB

Mapa de estaciones

Podemos usar cartopy para hacer un mapa y visualizar las estaciones de monitoreo en el pais

fig, ax = plt.subplots(subplot_kw={"projection":ccrs.PlateCarree()}, dpi=150)
ax.coastlines()
gl = ax.gridlines(draw_labels=True, crs=ccrs.PlateCarree())
ax.scatter(df_cat['longitud'], df_cat['latitud'], transform=ccrs.PlateCarree(), s=.5)
ax.add_feature(feature.LAND)
ax.add_feature(feature.OCEAN)
ax.add_feature(feature.COASTLINE, linewidth=.5)
ax.add_feature(feature.BORDERS, linewidth=.5)
<cartopy.mpl.feature_artist.FeatureArtist at 0x7f056e0af210>
/usr/share/miniconda3/envs/atmoscol2023/lib/python3.11/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_physical/ne_50m_land.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
/usr/share/miniconda3/envs/atmoscol2023/lib/python3.11/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_physical/ne_50m_ocean.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
/usr/share/miniconda3/envs/atmoscol2023/lib/python3.11/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_physical/ne_50m_coastline.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
/usr/share/miniconda3/envs/atmoscol2023/lib/python3.11/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_cultural/ne_50m_admin_0_boundary_lines_land.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
../../_images/e224e413231bfb22b8513911359316f1d13a8f75aefe4c2beea4573892c27292.png

podemos agrupar la data por área operativa, tipo de estacion, tecnologia, y otras variables

# df_grp = df_cat.groupby('AREA_OPERATIVA')
# df_grp = df_cat.groupby('TECNOLOGIA')
df_grp = df_cat.groupby('ESTADO')
fig, ax = plt.subplots(subplot_kw={"projection":ccrs.PlateCarree()}, dpi=150)

for _, group in df_grp:
    ax.scatter(group['longitud'], group['latitud'], transform=ccrs.PlateCarree(), s=.5, label=_)
    
ax.coastlines()
gl = ax.gridlines(draw_labels=True, crs=ccrs.PlateCarree())
ax.add_feature(feature.LAND)
ax.add_feature(feature.OCEAN)
ax.add_feature(feature.COASTLINE, linewidth=.5)
ax.add_feature(feature.BORDERS, linewidth=.5)
ax.legend(fontsize=5)
<matplotlib.legend.Legend at 0x7f056e0cff10>
../../_images/8adb57003b7a4e157063c5420ec0b73c0a726f7f53655ed7c01a4bcab75c7e83.png

Podemos validar el numero total de estaciones activas, en matenimiento y suspendidas de acuerdo con la información contenida en el catálogo

for grp in df_grp.groups.keys():
    print(f"{grp}: {len(df_grp.get_group(grp))}")
Activa: 2202
En Mantenimiento: 429
Suspendida: 1866

Mapa de estaciones interactivo

También podemos hacer mapas interactivos usando folium

import folium
from folium import plugins
from folium.plugins import MarkerCluster
min_lon, max_lon, min_lat, max_lat = -90, -72, -1, 14

map_ = folium.Map(location=[8, -76],
                  zoom_start = 6, 
                  min_lat=min_lat, 
                  max_lat=max_lat, 
                  min_lon=min_lon, 
                  max_lon=max_lon, 
                  zoom_control=False,
                  control_scale=True,
                  scrollWheelZoom=True,
                  width=1000,height=600)
marker_cluster = MarkerCluster(name="Estaciones").add_to(map_)

folium.TileLayer('cartodbpositron').add_to(map_)
folium.TileLayer('openstreetmap').add_to(map_)
folium.TileLayer('stamenterrain').add_to(map_)
folium.TileLayer('cartodbdark_matter').add_to(map_)
folium.LayerControl().add_to(map_)

minimap = plugins.MiniMap()
_ = map_.add_child(minimap)
# _

Ahora agregamos las estaciones usando la siguiente función

def plot_station(row):
    '''input: series that contains a numeric named latitude and a numeric named longitude
    this function creates a CircleMarker and adds it to your this_map'''
    
    html = row.to_frame("_").to_html(classes="table table-striped table-hover table-condensed table-responsive")
    popup = folium.Popup(html, max_width=2650)
    folium.Marker(location=[row.latitud, row.longitud], popup=popup).add_to(marker_cluster)
df_cat.apply(plot_station, axis=1)
map_
Make this Notebook Trusted to load map: File -> Trust Notebook

3. Acceso a la información historica de IDEAM usando datosabiertos.gov.co

la información historica de multiples sensores se puede consultar atraves de la plataforma de datos abiertos usando el aplicativo sodapy. Socrata utiliza un módulo denominado Socrata que permite realizar consultas al repositorio. Cada variable hidrometeorógica dispuesta se puede consultar usando el su respectivo código del set de datos.

Variable

Código del set de datos

Dirección del viento

kiw7-v9ta

Nivel instantaneo

bdmn-sqnh

Temperatura Minima del Aire

afdg-3zpb

Temperatura Maxima del Aire

ccvq-rp9s

Velocidad del Viento

sgfv-3yp8

Nivel Maximo

vfth-yucv

Nivel Minimo

pt9a-aamx

Humedad del Aire

uext-mhny

Temperatura

sbwg-7ju4

Nivel del mar mínimo

7z6g-yx9q

Nivel del mar máximo

uxy3-jchf

Nivel del mar

ia8x-22em

Presión Atmosferica

62tk-nxj5

Precipitación

s54a-sgyg

# importamos la libreria Socrata
from sodapy import Socrata

Precipitación (s54a-sgyg)

Vamos a consultar los datos de precipitación reportada en la página. por ende vamos a usar el código s54a-sgyg. Para esto usamos el metodo Socrata, pasamos la direccion del repositorio y None que corresponde a la no autenticación

# conexión cliente usando socrata al repositorio de datos abiertos
client = Socrata("www.datos.gov.co", None)
WARNING:root:Requests made without an app_token will be subject to strict throttling limits.

Una vez creado el cliente empezamos a hacer la consulta de datos usando client.get y pasando los respectivos parámetros dataset_identifier, de la tabla anterior , y limit para generar consultas no muy grandes para efectos demostrativos. El resultado es una lista con multiples diccionarios como se puede ver a continuación.

# Solicitud de informacion al repositorio de interés
results = client.get(dataset_identifier="s54a-sgyg", limit=2000)
results[:1]
[{'codigoestacion': '0035025090',
  'codigosensor': '0240',
  'fechaobservacion': '2019-02-27T01:00:00.000',
  'valorobservado': '0',
  'nombreestacion': 'BOSQUE INTERVENIDO  - AUT',
  'departamento': 'CUNDINAMARCA',
  'municipio': 'LA CALERA',
  'zonahidrografica': 'META',
  'latitud': '4.664888889',
  'longitud': '-73.84663889',
  'descripcionsensor': 'Precipitacion',
  'unidadmedida': 'mm'}]

Estos resultados los podemos convertir en un Dataframe usando pandas.Dataframe.from_records

results_df = pd.DataFrame.from_records(results)
results_df.head()
codigoestacion codigosensor fechaobservacion valorobservado nombreestacion departamento municipio zonahidrografica latitud longitud descripcionsensor unidadmedida
0 0035025090 0240 2019-02-27T01:00:00.000 0 BOSQUE INTERVENIDO - AUT CUNDINAMARCA LA CALERA META 4.664888889 -73.84663889 Precipitacion mm
1 0054035501 0240 2019-04-09T02:55:00.000 0 SANTIAGO GUTIERREZ - AUT VALLE DEL CAUCA ARGELIA SAN JUÁN 4.726388889 -76.11446944 Precipitacion mm
2 0015015120 0240 2016-04-29T07:00:00.000 0 UNIVERSIDAD TECNOLOGICA - AUT MAGDALENA SANTA MARTA CARIBE - GUAJIRA 11.22305556 -74.18591667 Precipitacion mm
3 0023105060 0240 2016-03-25T09:30:00.000 0 VEGACHA ANTIOQUIA VEGACHÍ MEDIO MAGDALENA 6.774 -74.797 Precipitacion mm
4 2120000109 0240 2019-06-22T08:17:00.000 0 COLEGIO RODOLFO LLINAS - AUT BOGOTA D.C. BOGOTA, D.C ALTO MAGDALENA 4.72 -74.108 Precipitacion mm

Ahora podemos usar filtrar los datos por diferentes campos como el codigoestacion, fechaobservacion, o valorobservado. Podemos pasar parametros SQL como where, AND, IN, entre otros, en el método client.get

# client.get?
# Solicitud de informacion para la estación de la Universidad Nacional - Bogotá - 0021205012
ppt_query = client.get(dataset_identifier="s54a-sgyg", 
                       select="fechaobservacion, valorobservado, codigoestacion", 
                       where="codigoestacion IN ('0021205012') \
                              AND fechaobservacion > '2017'")
df_est = pd.DataFrame.from_records(ppt_query)
df_est.head()
fechaobservacion valorobservado codigoestacion
0 2017-06-15T01:00:00.000 0 0021205012
1 2018-07-04T13:20:00.000 0 0021205012
2 2019-08-16T05:50:00.000 0 0021205012
3 2017-01-13T11:30:00.000 0 0021205012
4 2017-08-31T14:40:00.000 0 0021205012

Gráfico de la serie temporal

Podemos generar una serie temporal usando la información resultado de la consulta. Sin embargo, primero debemos revisar el tipo de dato resulta

df_est.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 3 columns):
 #   Column            Non-Null Count  Dtype 
---  ------            --------------  ----- 
 0   fechaobservacion  1000 non-null   object
 1   valorobservado    1000 non-null   object
 2   codigoestacion    1000 non-null   object
dtypes: object(3)
memory usage: 23.6+ KB
df_est['fechaobservacion'] = pd.to_datetime(df_est['fechaobservacion'])
df_est.set_index('fechaobservacion', inplace=True)
df_est.valorobservado = df_est['valorobservado'].astype(float)
df_est = df_est.sort_index()
df_est.tail()
valorobservado codigoestacion
fechaobservacion
2019-08-24 08:50:00 0.0 0021205012
2019-08-26 11:30:00 0.0 0021205012
2019-08-26 14:00:00 0.0 0021205012
2019-08-27 16:00:00 0.0 0021205012
2019-08-27 19:20:00 0.0 0021205012
# pd.options.plotting.backend = 'holoviews'
fig, ax = plt.subplots(figsize=(12, 3))
df_est['valorobservado'].plot(ax=ax, drawstyle="steps")
<Axes: xlabel='fechaobservacion'>
../../_images/fec9e9bcf237fe29e3bd052000372ae84f529036dde4a73486bcc1bf5feeaef5.png

Podemos solicitar informacion para estaciones que reportan datos en el último més

# Solicitud de informacion para la estación de la Universidad Nacional - Bogotá - 0021205012
ppt_query = client.get(dataset_identifier="s54a-sgyg", 
                       select="fechaobservacion, valorobservado, codigoestacion, nombreestacion", 
                       where="fechaobservacion > '2023-09-11'")
df_ult = pd.DataFrame.from_records(ppt_query)
df_ult.head()
fechaobservacion valorobservado codigoestacion nombreestacion
0 2023-09-11T00:01:00.000 0 3502500135 GUAYABETAL POLLO OLIMPICO - AUT
1 2023-09-11T00:03:00.000 0 3502500135 GUAYABETAL POLLO OLIMPICO - AUT
2 2023-09-11T00:04:00.000 0 3502500135 GUAYABETAL POLLO OLIMPICO - AUT
3 2023-09-11T00:05:00.000 0 0026177030 LA VIRGINIA
4 2023-09-11T00:05:00.000 0 3502500135 GUAYABETAL POLLO OLIMPICO - AUT

Temperatura (sbwg-7ju4)

De manera similar podemos consultar otros registros como los de temperatura. Cambiamos el identificador de set de datos y generamos una nueva consulta

# Solicitud de informacion para la estación de la Universidad Nacional - Bogotá - 0021205012
temp_query = client.get(dataset_identifier="sbwg-7ju4", 
                       select="fechaobservacion, valorobservado, codigoestacion", 
                       where="codigoestacion IN ('0021205012') \
                              AND fechaobservacion > '2017'")
df_temp = pd.DataFrame.from_records(temp_query)
df_temp.index = pd.to_datetime(df_temp['fechaobservacion'])
df_temp.valorobservado = df_temp['valorobservado'].astype(float)
df_temp = df_temp.sort_index()
df_temp.tail()
fechaobservacion valorobservado codigoestacion
fechaobservacion
2020-01-22 08:00:00 2020-01-22T08:00:00.000 13.6 0021205012
2020-01-22 10:00:00 2020-01-22T10:00:00.000 18.1 0021205012
2020-01-22 12:00:00 2020-01-22T12:00:00.000 17.6 0021205012
2020-01-22 16:00:00 2020-01-22T16:00:00.000 17.7 0021205012
2020-01-22 21:00:00 2020-01-22T21:00:00.000 13.8 0021205012
fig, ax = plt.subplots(figsize=(12, 3))
df_temp['valorobservado'].plot(c='C00', lw=0.5, ax=ax)
<Axes: xlabel='fechaobservacion'>
../../_images/c9d32fa9be82de501ac6d7880231454dff857a2fa7b16a5db68fc7a26825bab1.png

Resumen final

En este cuadernillo aprendimos una manera fácil y rápida para acceder a la información histórica de las estaciones hidrometeorológicas de IDEAM. De igual modo aprendimos a visualizar las estaciones usando mapas interactivos. También aprendimos a generar consultas a diferentes grupos de datos usando sintaxis SQL y el aplicativo socrata de la plataforma de datos abiertos del gobierno colombiano.

Resources and references

  • Rose, B. E. J., Kent, J., Tyle, K., Clyne, J., Banihirwe, A., Camron, D., May, R., Grover, M., Ford, R. R., Paul, K., Morley, J., Eroglu, O., Kailyn, L., & Zacharias, A. (2023). Pythia Foundations (Version v2023.05.01) https://doi.org/10.5281/zenodo.7884572